﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.IO;
using System.Threading;
using System.Net;
using System.Net.Sockets;
using System.Runtime.Serialization;
using System.Runtime.Serialization.Formatters.Binary;
using System.Diagnostics;

namespace SSFT
{
	public class SingleFileClient
	{
		enum ConnectionState
		{ Connecting, Established, Disconnected };

		string			m_hostname;
		int				m_port;

		volatile bool	runflag;
		Thread			work_thread;
		string			filepath;
		ProgressInfo	progress;
		ConnectionState	connect_state;
		volatile TcpClient client;
		BinaryFormatter	serializer;
		Exception		reported_exception;

		const int BufferSize = 1024;
		const int TimeoutPeriod = 5000;

		public SingleFileClient()
		{
			serializer = new BinaryFormatter();
		}

		public bool IsRunning
		{
			get { return work_thread != null; }
		}

		public string Filepath
		{
			get { return filepath; }
			set { filepath = value; }
		}

		public ProgressInfo Progress
		{
			get { return progress; }
		}

		public int Port
		{
			get { return m_port; }
			set { m_port = value; }
		}

		public string Hostname
		{
			get { return m_hostname; }
			set { m_hostname = value; }
		}

		public void Connect(string hostname, int port)
		{
			if (IsRunning)
				return;

			m_hostname = hostname;
			m_port = port;

			// Start the worker thread.
			work_thread = new Thread(new ThreadStart(Run));
			work_thread.Start();
		}

		public void Disconnect()
		{
			if (!IsRunning)
				return;

			// Make the worker thread stop.
			runflag = false;
			Log("Disconnecting client...");
		}

		public void Run()
		{
			byte[]		buffer = new byte[BufferSize];
			int			num_bytes;
			Stream		netstream = null, fstream = null;
			FileHeader	hdr;

			try
			{
				runflag = true;
				connect_state = ConnectionState.Connecting;
				Program.Form.ClientOptionsEnabled = false;
				Log("Connecting to host " +
					m_hostname + " on port " + m_port + "...");

				// Create ip4 tcp client.
				client = new TcpClient(AddressFamily.InterNetwork);
				client.SendTimeout = TimeoutPeriod;
				client.ReceiveTimeout = TimeoutPeriod;

				// Start the connection attempt.
				client.BeginConnect(
					m_hostname, m_port,
					new AsyncCallback(delegate(IAsyncResult result)
						{
							try
							{
								client.EndConnect(result);
								connect_state = ConnectionState.Established;
							}
							catch (Exception ex)
							{
								// If there was an exception in connecting
								// we store the exception for the worker to
								// catch.
								reported_exception = ex;
								connect_state = ConnectionState.Disconnected;
							}
						}), null);

				// Sleep as long as the client is connecting and
				// the thread should run indicated by runflag.
				while (connect_state == ConnectionState.Connecting)
				{
					if (!runflag)
						throw new Exception("Connection interrupted");
					Thread.Sleep(100);
				}

				// If the client is now disconnected, an exception occurred,
				// then we throw that exception.
				if (connect_state == ConnectionState.Disconnected)
				{
					throw reported_exception;
				}

				// If the client is now connected, we start the file
				// receiving process.
				if (connect_state == ConnectionState.Established)
				{
					Log("Connection successful");

					netstream = client.GetStream();
					hdr = serializer.Deserialize(netstream) as FileHeader;

					Log("Server sends file " +
						hdr.m_filename);

					// Now that we know the filename and filesize of the server's
					// file, we prompt the user for where to store the file.
					filepath = null;
					Program.Form.ClientRequestFilepath(hdr.m_filename);
					while (filepath == null)
					{
						if (!runflag)
							throw new Exception("File transfer interrupted");
						Thread.Sleep(100);
					}
					fstream = new FileStream(filepath, FileMode.Create, FileAccess.Write);

					// Let the server know we're ready to receive.
					netstream.Write(buffer, 0, 1);

					// Create a progress object for the gui.
					progress = new ProgressInfo();
					progress.Filename = hdr.m_filename;
					progress.Filesize = hdr.m_filesize;

					// Start receiving the file.
					Log("File transfer starting...");
					while (fstream.Position < hdr.m_filesize)
					{
						if (!runflag)
							throw new Exception("File transfer interrupted");

						num_bytes = netstream.Read(buffer, 0, BufferSize);
						if (num_bytes <= 0)
							throw new Exception("Connection reset by peer");
						fstream.Write(buffer, 0, num_bytes);

						Progress.BytesTransferred = fstream.Position;
					}
					Log("File transfer complete");
					Log("File saved as " +
						filepath);
				}
			}
			catch (Exception ex)
			{
				Log(Util.ExceptionToString(ex));
				if (filepath != null && fstream != null)
				{
					fstream.Close();
					File.Delete(filepath);
				}
			}
			finally
			{
				if (fstream != null)
					fstream.Close();
				if (client != null)
					client.Close();

				Program.Form.ClientOptionsEnabled = true;
				Log("Client disconnected");

				// Reset all states.
				connect_state = ConnectionState.Disconnected;
				progress = null;
				filepath = null;
				runflag = false;
				work_thread = null;
			}
		}

		private void Log(string msg)
		{
			Program.Form.ClientLog(msg);
		}
	}
}
